The plugin is a single Python file (plugin.py) organized as follows:
Top Section (Lines 1-30):
PLUGIN_NAME, PLUGIN_VERSION)Helper Classes (Lines 31-70):
ToolTip: Provides hover tooltips for UI widgetsUtility Functions (Lines 71-180):
ensure_config_dir(): Creates config directoryget_saved_configs(): Lists saved configuration filesgenerate_unique_placeholder(): Creates UUID-based placeholdersstrip_css_comments(): Removes and saves CSS commentsrestore_css_comments(): Restores CSS commentsis_unsafe_shorthand(): Detects complex shorthand patternsvalidate_backup_folder(): Validates backup directoryMain Entry Point (Lines 181-400):
run(bk): Plugin entry point called by Sigil
build_patterns(cfg): Constructs regex patternsshow_file_selector()UI Functions (Lines 401-end):
show_config_window(): Configuration dialogshow_unsafe_css_dialog(): Unsafe pattern warningshow_shorthand_preview_dialog(): Safe shorthand previewshow_preview_window(): All changes previewshow_file_selector(): Main file selection windowStep 1: Add to keywords list in build_patterns() (around line 220):
keywords = [
# ... existing keywords
('new-keyword', 'newkeyword', 'newkeyword_unit'),
]
Step 2: Add default values in run() function (around line 195):
config = {
# ... existing configs
'newkeyword': prefs.get('newkeyword', '1.5'),
'newkeyword_unit': prefs.get('newkeyword_unit', 'em'),
}
Step 3: Add to UI in show_config_window() (around line 550):
keywords = [
# ... existing keywords
('new-keyword', 'newkeyword'),
]
Step 4: Add to defaults in reset_defaults() (around line 730):
defaults = {
# ... existing defaults
'newkeyword': '1.5',
}
To add support for ch, vw, or other CSS units:
Step 1: Modify radio button creation in show_config_window() (around line 580):
# Add new radio button column
ch_header = Label(config_frame, text="ch", font=('Arial', 10, 'bold'), width=5)
ch_header.grid(row=0, column=4, pady=5)
# In the loop for each keyword:
Radiobutton(config_frame, text="", variable=unit_var,
value='ch').grid(row=idx, column=4, pady=3)
Step 2: Update bulk set buttons:
def set_all_ch():
for unit_var in unit_vars.values():
unit_var.set('ch')
all_ch_btn = Button(bulk_frame, text="All ch", command=set_all_ch, width=10)
all_ch_btn.pack(side=LEFT, padx=5)
To add validation to is_unsafe_shorthand() (around line 155):
def is_unsafe_shorthand(css_line):
# ... existing checks
# New rule: reject if contains attr() function
if re.search(r'\battr\s*\(', font_value, re.IGNORECASE):
return True
return False
Changing window size: Look for .geometry() calls:
# In show_config_window():
window_width = min(750, int(screen_width * 0.8)) # Adjust multiplier
Changing colors: Search for color codes:
# Find and replace hex codes like:
background='#ffd6d6' # Pink
background='#cce5ff' # Blue
foreground='#FF0000' # Red
Adding new checkbox to config: Add after existing checkboxes (around line 615):
new_feature_var = BooleanVar(value=config.get('new_feature', 'false') == 'true')
new_check = Checkbutton(frame, text="Enable new feature", variable=new_feature_var)
new_check.pack(anchor=W, pady=(0, 5))
To add a filter for SVG files in the file selector (around line 1180):
Step 1: Add checkbox variable:
show_svg = BooleanVar(value=False)
Step 2: Add checkbox to filter_frame:
cb_svg = Checkbutton(filter_frame, text="Show SVG Files", variable=show_svg,
command=lambda: [show_all.set(False), show_text.set(False),
show_css.set(False), update_filter()])
cb_svg.pack(side=LEFT, padx=5)
Step 3: Modify populate_checkboxes():
if show_svg.get():
filtered_files = [f for f in files_with_keywords if f['type'] == 'svg']
Create a mock book container object:
class MockBook:
def __init__(self):
self.prefs = {}
self.files = {
'css1': 'body { font-size: small; }',
'html1': '<p style="font-size: large;">Test</p>'
}
def getPrefs(self):
return self.prefs
def savePrefs(self, prefs):
self.prefs = prefs
def css_iter(self):
return [('css1', 'style.css')]
def text_iter(self):
return [('html1', 'page.html')]
def readfile(self, file_id):
return self.files.get(file_id, '')
def writefile(self, file_id, content):
self.files[file_id] = content
# Test:
bk = MockBook()
run(bk)
# Debug pattern matching:
pattern = compiled_patterns[0][0] # First pattern
test_line = "font-size: small;"
print(f"Pattern: {pattern.pattern}")
print(f"Match: {pattern.search(test_line)}")
# Test placeholder uniqueness:
css = "/* test */ body { font-size: small; }"
text, pmap = strip_css_comments(css)
print(f"Placeholders: {list(pmap.keys())}")
restored = restore_css_comments(text, pmap)
assert restored == css
# Debug checkbox state:
for widget in checkbox_frame.winfo_children():
print(f"{widget} state: {widget['state']}")
# Test backup validation:
is_valid, error = validate_backup_folder('/path/to/folder')
print(f"Valid: {is_valid}, Error: {error}")
Add debug prints throughout:
def strip_css_comments(css_text):
print(f"DEBUG: Input length: {len(css_text)}")
# ... processing
print(f"DEBUG: Found {len(placeholder_map)} comments")
return text_no_comments, placeholder_map
To disable in production, use a global flag:
DEBUG = False # Set at top of file
def debug_print(msg):
if DEBUG:
print(msg)
sys.stdout.flush()
import re
keyword = 'small'
escaped = re.escape(keyword)
pattern = rf'(font-size\s*:\s*){escaped}(\s*(?=[;}}"\'\!]|$))'
regex = re.compile(pattern, re.IGNORECASE | re.MULTILINE)
test_cases = [
'font-size: small;',
'font-size:small;',
'font-size: SMALL;',
'font-size: small !important;',
'font-size: smaller;', # Should not match
]
for test in test_cases:
match = regex.search(test)
print(f"{test:30} -> {bool(match)}")
Windows:
os.path.join() alwaysmacOS:
Linux:
.sigil folder creation permissionsfont: small caption; /* 'caption' is system font, not family */
Plugin will convert 'small' but result may be incorrect.
font-size: large; font-size: small; /* Last one wins */
Plugin converts both, even though only last matters.
[title~="small"] { font-size: 12px; }
Pattern won't match (correct behavior), but worth noting.
--my-size: small; /* Variable assignment */
font-size: var(--my-size); /* Usage */
Plugin won't convert variable assignment or usage (correct for safety).
If a CSS line exceeds ~10,000 characters, regex performance may degrade significantly.
Scaling:
Memory usage:
Regex Performance:
Canvas Scrolling:
update_idletasks() before setting scroll regionDialog Modality:
transient() + grab_set() required for proper modal dialogslift() to bring to frontText Widget Performance:
Tested with:
API assumptions:
bk.css_iter() returns (file_id, href) tuplesbk.text_iter() returns (file_id, href) tuplesbk.readfile() accepts file_id, returns bytes or stringbk.writefile() accepts file_id and stringbk.getPrefs() / bk.savePrefs() for persistenceBreaking changes to watch for:
Decision: Use uuid.uuid4().hex for comment placeholders
Rationale:
___COMMENT_0___, etc.) could exist in original CSSRejected alternatives:
Decision: Always store originals in memory, optionally write to disk
Rationale:
Rejected alternatives:
Decision: Separate "Unsafe CSS Detected" and "Review Safe Shorthand Changes"
Rationale:
Rejected alternatives:
Decision: Individual checkboxes for each file
Rationale:
Rejected alternatives:
Simplicity vs Features:
Performance vs Safety:
Flexibility vs Usability:
Create two test files (provided separately):
test-styles.css: External CSStest-page.html: HTML with embedded styles and inline attributesBasic (Shorthand Disabled):
font-size: changes shownShorthand (Enabled):
font-size: and safe font: convertedEdge Cases:
After any code change, verify:
Plugin is ready for release when:
Format: MAJOR.MINOR.PATCH
Increment:
Current: 0.7.0
PLUGIN_VERSION)File structure:
plugin.xml example:
<?xml version="1.0" encoding="UTF-8"?>
<plugin>
<name>CSS Font-Size Keyword Converter</name>
<type>edit</type>
<author>Your Name</author>
<description>Converts CSS font-size keywords to em/rem units</description>
<version>0.7.0</version>
<engine>python3.4</engine>
</plugin>
Creating plugin ZIP:
NewUnit/ folderNewUnit_v0.7.0.zipOfficial Sigil Plugin Repository:
Submit to: https://www.mobileread.com/forums/forumdisplay.php?f=237
GitHub Release:
v0.7.0Release Notes Template:
## Version 0.7.0 - YYYY-MM-DD
### New Features
- Feature description
### Bug Fixes
- Fix description
### Known Issues
- Issue description
### Upgrade Notes
- Migration instructions if needed
Keep CHANGELOG.md updated with:
Functions:
lowercase_with_underscores()validate_folder(), show_dialog()is_*() for boolean predicatesVariables:
lowercase_with_underscoresfile_checkboxes not fc*_frame, *_btn, *_label, *_varConstants:
UPPERCASE_WITH_UNDERSCORESPLUGIN_NAME, PLUGIN_VERSION, CONFIG_DIRExceptions:
i, idx, e (for exceptions)Use Global Scope For:
Use Local Scope For:
Avoid:
File Operations:
try:
# operation
except (IOError, OSError) as e:
print(f'Error: {e}')
sys.stdout.flush()
# continue or return
User Input Validation:
try:
value = float(input_str)
if value <= 0:
show_error("Must be positive")
return False
except ValueError:
show_error("Must be a number")
return False
Always:
sys.stdout.flush()Module/Function Docstrings:
def function_name(param):
"""
Brief description of what function does
Args:
param: Description
Returns:
Description of return value
"""
Inline Comments:
# TODO: descriptionSection Headers:
# ===== SECTION NAME =====
# For major sections of code
Minimum: Python 3.4 (matches Sigil's requirement)
Recommended: Python 3.8+
Tested: Python 3.9, 3.10, 3.11
Features used that require 3.4+:
To support Python 3.4-3.5:
Replace f-strings with .format():
# Instead of:
f"Error: {msg}"
# Use:
"Error: {}".format(msg)
All imports are from standard library:
re: Regex pattern matchingsys: stdout flushing, exit codesos: Path operations, directory creationjson: Configuration file formatuuid: Unique placeholder generationdatetime: Timestamp generation for backupstkinter: GUI frameworkNo third-party dependencies - Plugin is self-contained.
Python 3.x uses tkinter (lowercase)
from tkinter import *
from tkinter import ttk, filedialog, messagebox
Not compatible with Python 2.x (which used Tkinter)
Tkinter features used:
Book Container Methods Used:
Assumptions:
readfile() may return bytes or string (plugin handles both)Version Notes:
File Paths:
os.path.join(), never hardcode / or \Tkinter Rendering:
Configuration Storage:
~/.sigil/ on macOS/Linux%APPDATA%/sigil/ on Windowsos.path.expanduser('~') handles bothLine Endings:
Permissions:
Plugin file: plugin.py
Config storage: ~/.sigil/NewUnit/configs/*.json
Temp backup test: <backup_folder>/.write_test_<uuid>.tmp
| Function | Purpose | Returns |
|---|---|---|
run(bk) |
Main entry point | 0 on success |
build_patterns(cfg) |
Create regex patterns | List of (pattern, replacer, type) |
strip_css_comments(text) |
Remove comments | (text, placeholder_map) |
restore_css_comments(text, map) |
Restore comments | text |
is_unsafe_shorthand(line) |
Check safety | bool |
validate_backup_folder(path) |
Check writability | (bool, error_msg) |
| Key | Type | Default | Description |
|---|---|---|---|
xxsmall |
string | '0.6' | xx-small value |
xxsmall_unit |
string | 'em' | xx-small unit |
| (repeat for all 9 keywords) | |||
convert_font_shorthand |
string | 'false' | Enable shorthand |
create_backup |
string | 'false' | Enable external backup |
backup_path |
string | '' | Backup folder path |
| Element | Color | Hex |
|---|---|---|
| Regular "before" | Pink | #ffd6d6 |
| Shorthand "before" | Blue | #cce5ff |
| [SHORTHAND] label | Red | #FF0000 |
| "after" | Green | #d6ffd6 |
| File headers | Blue | #2196F3 |
| Warnings | Orange | #FF6600 |
| Code | Meaning |
|---|---|
| 0 | Success |
| -1 | Not run from Sigil |
Document Version: 1.0
Last Updated: 2025-01-15
Contact: [maintainer email/github]